<?php

/**
 * Feel free to use this in any way, no restrictions. Delete, modify
 * and/or take credit for any part of this code as you please.
 * 
 * Note: This class assumes a session has already been started if
 * $autoAddSessionId is true.
 * 
 * @author tyleregeto, tyleregeto.com
 */
class Nonces {
	private $_expiryTime;
	private $_db;
	private $_salt;
	
	/**
	 * Default expiry time (86400) is 24 hours
	 * 
	 * Db is a PDO database connection.
	 * 
	 * Database structure is as follows:
	 * 
	 * CREATE TABLE IF NOT EXISTS `nonces` (
	 *	  `nonce` char(32) NOT NULL,
	 *	  `timestamp` datetime NOT NULL,
	 *	  PRIMARY KEY `nonces_nonce_timestamp` (`nonce`, `timestamp`)
	 *	) ENGINE=InnoDB DEFAULT CHARSET=utf8;
	 */
	public function __construct($db, $expiryInSecs = 86400, $salt = 'G6d7$5Fjk1X') {
		$this->_db = $db;
		$this->_salt = $salt;
		$this->_expiryTime = $expiryInSecs;
	}
	
	/**
	 * $valuesToUse is an array of any values you wish to be included
	 * in the nonce hash generation.
	 * 
	 * example array('items/delete', '34', '4567');
	 * 
	 * The values above would be: [requested url], [item to be deleted id], [the current users id]
	 * 
	 * $autoAddSessionId ties the nonce to the current user session. This is the recommended behaviour.
	 */
	public function createNewNonce(array $valuesToUse, $autoAddSessionId = true) {
		$date = new DateTime();
		$timestamp = $date->format('U');
		
		$fields = array(
			'timestamp' => $timestamp,
			'nonce' => $this->makeNonce($valuesToUse, $timestamp, $autoAddSessionId),
		);
		
		return $fields;
	}
	
	/**
	 * Creates a nonce using the supplied timestamp and values. A helper method for
	 * verifying nonce values.
	 */
	public function recreateNonce(array $valuesToUse, $timestamp, $autoAddSessionId = true) {
		return $this->makeNonce($valuesToUse, $timestamp, $autoAddSessionId);
	}
	
	/**
	 * Marks a nonce as used so it can never be used again.
	 */
	public function markNonceAsUsed($nonce) {
		$cleanNonce = $this->_db->quote($nonce);
		$cleanTimestamp = (int) $timestamp;
		$sql = "INSERT INTO nonces (nonce, timestamp) VALUES($cleanNonce, $cleanTimestamp)";
		
		return $this->_db->query($sql);
	}
	
	public function isValid($nonce, $timestamp) {
		$cleanNonce = $this->_db->quote($nonce);
		$cleanTimestamp = (int) $timestamp;
		
		$expiresDate = $cleanTimestamp + $this->_expiryTime;
		$now = new DateTime();
		
		if($expiresDate - $now->format("U") < 0) {
			//expired
			return false;
		} else {
			//make sure its no registered in the database
			$sql = "SELECT (COUNT(nonce) > 0) AS is_used FROM nonces WHERE nonce = $cleanNonce & timestamp = $cleanTimestamp";
			$result = $this->_db->query($sql);
			
			if($result === false) {
				throw new Exception('Something went wrong on nonce DB lookup');
			}
			else {
				$row = $result->fetch();
				return $row['is_used'];
			}
		}
	}
	
	/**
	 * Make sure you understand PHP type casting. Only accepts strings,
	 * numeric values, and booleans.
	 * 
	 * http://php.net/manual/en/language.types.type-juggling.php
	 */
	private function makeNonce($valuesToUse, $timestamp, $addSessionId) {
		$base = $this->_salt;
		$values = $valuesToUse;
		
		if($addSessionId) {
			$values['__session_id'] = session_id();
		}
		
		foreach($values as $key => $v) {
			if(!is_numeric($v) && !is_string($v) && !is_bool($v)) {
				//no messing around here, it must be a valid type.
				throw Exception('Invalid value type used while making nonce');
				return ;
			}
			
			$base .= (string) $v;
		}
		
		return md5($base . $timestamp);
	}
}

?>